using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Dsp
{
    /// <summary>
    /// Reads a *mono* WAV file. Supports different sample formats:
    /// * PCM08: 08-bit Linear PCM
    /// * PCM16: 16-bit Linear PCM
    /// * PCM24: 24-bit Linear PCM
    /// * FLT32: 32-bit Floating Point (IEEE 754)
    /// 
    /// Note: The opened stream is automaticaly closed when read pointer reaches the EOF, and then
    /// the property-flag Closed is asserted.   
    /// 
    /// Warning: this class only reads "sane" WAV files, i.e, files that contains only one chunk of 
    /// data and that have "fmt " chunk in first place. There valid WAV files out there which can't
    /// be read with this class.
    /// 
    /// Warning: Dinamic range of PCM formats is limitied to range [-1, +1)
    /// 
    /// References:
    /// http://www.sonicspot.com/guide/wavefiles.html
    /// </summary>
    public class WavFileReader 
    {

        private WavFormat    _format;
        private int          _fs;
        private string       _filename;           // if aplicable, if not null
        private Stream       _stream;             // generic stream with CanSeek := true
        private BinaryReader _reader;
        private int          _sampleCount;        // number of samples read
        private int          _sampleXferd;        // number of samples tranfered
        private bool         _closed;             // EOF and closed stream?
        private bool         _valid;              // Is a valid WAV file?

        private ushort       _audioFormat;
        private ushort       _numChannels;   
        private uint         _byteRate;
        private ushort       _blockAlign;
        private ushort       _bitsPerSample;
        
        #region Constructors
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="filename">File name created wiwth FileMode.Create (OVERWRITE MODE!)</param>
        /// <param name="format">Sample format</param>
        /// <param name="fs">Sampling frequency</param>
        public WavFileReader(string filename)
        {
            _filename = filename;
            _stream = new FileStream(_filename, FileMode.Open);

            Constructor(_stream);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="stream">Any Stream Class derived object with the property CanSeek set to true
        /// like files and memory streams but it isn't valid sockets, serial ports or the like streams
        /// </param>
        /// <param name="format">Sample format</param>
        /// <param name="fs">Sampling frequency</param>
        public WavFileReader(Stream stream)
        {
            Constructor(stream);
        }


        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="type">Access type; for writting or reading</param>
        /// <param name="filename">File name to CREATE for write</param>
        private void Constructor(Stream stream)
        {

            byte[]        chunkID; // chunk ID string
            int           chunkSZ; // chunk size
            ASCIIEncoding ascii    = new ASCIIEncoding();
            
            _stream      = stream;
            _reader      = new BinaryReader(_stream);
            _sampleXferd = 0;
            _closed      = false;
            

            // RIFF Chunk -------------------------------------------------------------------------
            chunkID = _reader.ReadBytes(4);
            chunkSZ = _reader.ReadInt32();
            if (ascii.GetString(chunkID) != "RIFF") goto NoValid;

            chunkID = _reader.ReadBytes(4);
            if (ascii.GetString(chunkID) != "WAVE") goto NoValid;

            // FMT_ Chunk -------------------------------------------------------------------------
            chunkID = _reader.ReadBytes(4);
            chunkSZ = _reader.ReadInt32();
            if (ascii.GetString(chunkID) != "fmt ") goto NoValid;

            _audioFormat   = _reader.ReadUInt16();
            _numChannels   = _reader.ReadUInt16();
            _fs            = _reader.ReadInt32();
            _byteRate      = _reader.ReadUInt32();
            _blockAlign    = _reader.ReadUInt16();
            _bitsPerSample = _reader.ReadUInt16();

            // skip extra bytes            
            _stream.Seek((int) chunkSZ - 16, SeekOrigin.Current);
            
            if (_numChannels != 1)              goto NoValid;
            
            _format = WavFormat.UNKNOWN;

            if (_audioFormat == 0x0001)
            {
                // PCM
                if (_bitsPerSample == 08) { _format = WavFormat.PCM08; }
                if (_bitsPerSample == 16) { _format = WavFormat.PCM16; }
                if (_bitsPerSample == 24) { _format = WavFormat.PCM24; }
            }

            if (_audioFormat == 0x0003)
            {
                // Floating Point IEEE
                if (_bitsPerSample == 32) { _format = WavFormat.FLT32; }                
            }

            if (_format == WavFormat.UNKNOWN) 
                goto NoValid;


            // ???? Chunk -------------------------------------------------------------------------
            // Skip all subsequent chunks but "data"
            while (true)
            {
                chunkID = _reader.ReadBytes(4);
                chunkSZ = _reader.ReadInt32();
                if (ascii.GetString(chunkID) == "data") 
                    break;

                _stream.Seek((int)chunkSZ, SeekOrigin.Current);
            }

            _valid = true;
            _sampleCount = chunkSZ * 8 / _bitsPerSample;
            return;

          
            NoValid:
            _valid = false;
            Close();
            return;
        }
        #endregion

     
        /// <summary>
        /// Reads audio samples from the WAV file
        /// </summary>
        /// <param name="buffer">Array with data</param>
        /// <param name="offset">Offset in the array</param>
        /// <param name="count">Number of samples that will be read</param>
        public int XferData(double[] buffer, int offset, int count)
        {  
            // samples in different formats
            double s;

            byte  s08;
            short s16;
            int   s24;            
            byte  s24_1;
            byte  s24_2;
            byte  s24_3;
            float s32;

            int items = 0;

            for (int i = offset; i < offset + count; i++)
            {
                if (_sampleCount == _sampleXferd)
                {
                    // there is no more samples in the stream: EOF
                    Close();
                    break;
                }

                switch (_format)
                {
                    case WavFormat.PCM08:
                        // 8-bit samples are always stores as UNSIGNED (centered at 128)
                        s08   = _reader.ReadByte(); 
                        s     = (((double) s08) / 255) * 2 - 1; 
                        break;

                    case WavFormat.PCM16: 
                        s16   = _reader.ReadInt16(); 
                        s     = (double) s16 / 0032768.0; 
                        break;

                    case WavFormat.PCM24:
                        s24_1 = _reader.ReadByte();
                        s24_2 = _reader.ReadByte();
                        s24_3 = _reader.ReadByte();
                        
                        // keep the sign bit, stupid!                        
                        if ((s24_3 & 0x80) != 0)
                            unchecked { s24 = (int) 0xFF000000; }
                        else
                            unchecked { s24 = (int) 0x00000000; }
                        
                        s24 |= (s24_3 << 16) | (s24_2 << 08) | s24_1;
                        s   = (double) s24 / 8388607.0; 
                        break;

                    case WavFormat.FLT32: 
                        s32   = _reader.ReadSingle();
                        s     = (double) s32;
                        break;
                    
                    default:
                        s = 0;
                        break;
                }


                buffer[i] = s;
                _sampleXferd++;
                items++;
            }

            return items;
        }
        
        ~WavFileReader()
        {
            if (!_closed)
                Close();
        }


        #region Properties
        public bool   Closed         { get { return _closed;  } }
        public Stream Stream         { get { return _stream;  } }
        public WavFormat SampleFormat   { get { return _format;  } }
        public int    Fs             { get { return _fs;      } }
        public bool   Valid          { get { return _valid;   } }
        #endregion

        /// <summary>
        /// Close the open stream (file)
        /// </summary>
        public void Close()
        {
            _reader.Close();
            _stream.Close();
            _closed = true;
        }
    }
}